Skip to content

2692. 使对象不可变

2692. 使对象不可变

ts
type JSONValue =
  | null
  | boolean
  | number
  | string
  | JSONValue[]
  | { [key: string]: JSONValue }
type Obj = Array<JSONValue> | Record<string, JSONValue>

const METHODS = ['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'reverse']

const isObject = (obj: any) => typeof obj === 'object' && obj !== null

const proxyHandler: ProxyHandler<any> = {
  get: (target, props) => {
    // 获取当前访问属性
    const val = Reflect.get(target, props)
    // 非对象返回值
    if (!isObject(val)) return val
    // 递归代理
    return proxify(val)
  },
  set: (target, props) => {
    // 直接修改数组,抛出异常
    if (Array.isArray(target)) {
      throw `Error Modifying Index: ${String(props)}`
    }
    // 抛出对象异常修改
    throw `Error Modifying: ${String(props)}`
  },
}

function makeImmutable(obj: Obj): any {
  return proxify(obj)
}

const methodHandler: ProxyHandler<(...args: any[]) => any> = {
  apply(target) {
    throw `Error Calling Method: ${target.name}`
  },
}

function proxify<T extends Obj>(obj: T): T {
  if (Array.isArray(obj)) {
    // 遍历拦截数组操作方法
    METHODS.forEach((method: (typeof METHODS)[number]) => (
      (obj as any)[method] = new Proxy((obj as any)[method], methodHandler)
    ))
  }

  return new Proxy(obj, proxyHandler) as T
}

Object.freeze 为什么不够

Object.freeze浅层不可变。它阻止顶层属性的修改和新增,但嵌套对象仍然可变:

js
const obj = Object.freeze({ a: { b: 1 } });
obj.a.b = 2;  // ✅ 不会报错,嵌套对象没有被 freeze

要实现深度不可变,需要递归冻结所有嵌套对象。这道题用 Proxy 而非 Object.freeze 来实现——区别在于 Proxy 可以惰性地拦截访问,而不需要初始化时遍历整个对象树。

两层代理

代码中有两个 ProxyHandler:

proxyHandler 拦截对象的 getset

  • get 获取属性值,如果值是对象就递归 proxify——惰性深度代理
  • set 禁止任何修改,根据 target 是数组还是对象抛出不同的错误

methodHandler 拦截数组变异方法的调用,通过包装方法为 Proxy 实现。当调用 arr.push(1) 时,实际上是先 getpush 方法(返回一个 Proxy),再 apply 调用它(被 methodHandler 拦截抛错)。

get 中的无声递归

注意 proxify 对非对象的子属性不创建 Proxy,对对象子属性则创建新 Proxy。这意味着每次访问嵌套对象都会产生一个新的 Proxy 实例——这种惰性策略的代价是:对同一嵌套对象的多次访问会产生不同的 Proxy 包装器,严格相等比较 === 会失败。